/* Copyright (c) 2009 Lintorus@EU-Destromath
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package authenticator;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.security.SecureRandom;
import java.util.Random;

import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.engines.RSAEngine;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.RSAKeyParameters;

import authenticator.http.HttpConnection;
import authenticator.http.HttpRequest;
import authenticator.http.HttpResponse;

public class ServerCommunication {

    private static final String INITIALIZATION_URL = "http://mobile-service.blizzard.com/enrollment/enroll.htm";
    private static final String TIME_URL = "http://mobile-service.blizzard.com/enrollment/time.htm";

    private static final String MOBILE_URL = "http://mobile-service.blizzard.com";
    private static final String RESTORE_PATH = "/enrollment/initiatePaperRestore.htm";
    private static final String RESTOREVALIDATE_PATH = "/enrollment/validatePaperRestore.htm";

    private static final String DEFAULT_REGION = "US";
    private static final String DEFAULT_MOBILE_MODEL = "Motorola RAZR v3";

    private static final BigInteger ms_modulus = new BigInteger(1, new byte[] {
            (byte)0x95, (byte)0x5e, (byte)0x4b, (byte)0xd9,
            (byte)0x89, (byte)0xf3, (byte)0x91, (byte)0x7d,
            (byte)0x2f, (byte)0x15, (byte)0x54, (byte)0x4a,
            (byte)0x7e, (byte)0x05, (byte)0x04, (byte)0xeb,
            (byte)0x9d, (byte)0x7b, (byte)0xb6, (byte)0x6b,
            (byte)0x6f, (byte)0x8a, (byte)0x2f, (byte)0xe4,
            (byte)0x70, (byte)0xe4, (byte)0x53, (byte)0xc7,
            (byte)0x79, (byte)0x20, (byte)0x0e, (byte)0x5e,
            (byte)0x3a, (byte)0xd2, (byte)0xe4, (byte)0x3a,
            (byte)0x02, (byte)0xd0, (byte)0x6c, (byte)0x4a,
            (byte)0xdb, (byte)0xd8, (byte)0xd3, (byte)0x28,
            (byte)0xf1, (byte)0xa4, (byte)0x26, (byte)0xb8,
            (byte)0x36, (byte)0x58, (byte)0xe8, (byte)0x8b,
            (byte)0xfd, (byte)0x94, (byte)0x9b, (byte)0x2a,
            (byte)0xf4, (byte)0xea, (byte)0xf3, (byte)0x00,
            (byte)0x54, (byte)0x67, (byte)0x3a, (byte)0x14,
            (byte)0x19, (byte)0xa2, (byte)0x50, (byte)0xfa,
            (byte)0x4c, (byte)0xc1, (byte)0x27, (byte)0x8d,
            (byte)0x12, (byte)0x85, (byte)0x5b, (byte)0x5b,
            (byte)0x25, (byte)0x81, (byte)0x8d, (byte)0x16,
            (byte)0x2c, (byte)0x6e, (byte)0x6e, (byte)0xe2,
            (byte)0xab, (byte)0x4a, (byte)0x35, (byte)0x0d,
            (byte)0x40, (byte)0x1d, (byte)0x78, (byte)0xf6,
            (byte)0xdd, (byte)0xb9, (byte)0x97, (byte)0x11,
            (byte)0xe7, (byte)0x26, (byte)0x26, (byte)0xb4,
            (byte)0x8b, (byte)0xd8, (byte)0xb5, (byte)0xb0,
            (byte)0xb7, (byte)0xf3, (byte)0xac, (byte)0xf9,
            (byte)0xea, (byte)0x3c, (byte)0x9e, (byte)0x00,
            (byte)0x05, (byte)0xfe, (byte)0xe5, (byte)0x9e,
            (byte)0x19, (byte)0x13, (byte)0x6c, (byte)0xdb,
            (byte)0x7c, (byte)0x83, (byte)0xf2, (byte)0xab,
            (byte)0x8b, (byte)0x0a, (byte)0x2a, (byte)0x99
    });
    private static final BigInteger ms_exponent = new BigInteger("257");

    public static AuthData restore(String serial, String restoreCode) throws IOException{
        byte[] serialBytes = null;
        try {
            serialBytes = serial.toUpperCase().replace("-", "").getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new IOException(e);
        }
        byte[] challenge = initiateRestore(serialBytes);


        // only take the first 10 bytes of the restore code and encode to byte taking count of the missing chars
        byte[] restoreCodeBytes = new byte[10];
        char[] arrayOfChar = restoreCode.toUpperCase().toCharArray();
        for (int i = 0; i < 10; i++)
        {
            restoreCodeBytes[i] = Util.convertRestoreCodeCharToByte(arrayOfChar[i]);
        }

        // build the response to the challenge
        HMac hmac = new HMac(new SHA1Digest());
        hmac.init(new KeyParameter(restoreCodeBytes));
        byte[] hashdata = new byte[serialBytes.length + challenge.length];
        System.arraycopy(serialBytes, 0, hashdata, 0, serialBytes.length);
        System.arraycopy(challenge, 0, hashdata, serialBytes.length, challenge.length);
        hmac.update(hashdata, 0, hashdata.length);
        byte[] hash = new byte[hmac.getMacSize()];
        hmac.doFinal(hash, 0);

        // create a random otp
        byte[] oneTimePad = createOneTimePad(20);

        // concatenate the hash and key
        byte[] hashkey = new byte[hash.length + oneTimePad.length];
        System.arraycopy(hash, 0, hashkey, 0, hash.length);
        System.arraycopy(oneTimePad, 0, hashkey, hash.length, oneTimePad.length);

        // encrypt the data with BMA public key
        RSAEngine rsa = new RSAEngine();
        rsa.init(true, new RSAKeyParameters(false, ms_modulus, ms_exponent));
        byte[] encrypted = rsa.processBlock(hashkey, 0, hashkey.length);

        // prepend the serial to the encrypted data
        byte[] postbytes = new byte[serialBytes.length + encrypted.length];
        System.arraycopy(serialBytes, 0, postbytes, 0, serialBytes.length);
        System.arraycopy(encrypted, 0, postbytes, serialBytes.length, encrypted.length);

        HttpResponse response = HttpConnection.doHttpRequest(
                new HttpRequest(HttpRequest.REQUEST_TYPE_POST, MOBILE_URL+RESTOREVALIDATE_PATH, postbytes));

        if (response.getResponseCode() != HttpURLConnection.HTTP_OK) {
            throw (new IOException("Restore validate request returned HTTP error " + Integer.toString(response.getResponseCode())));
        }
        if (response.getResponseBody().length != 20) {
            throw (new IOException("Restore validate request returned invalid length HTTP body"));
        }

        byte[] responseBody = response.getResponseBody();
        byte[] secretKey = doOneTimePadDecryption(responseBody, oneTimePad);

        return new AuthData("US", serial, secretKey, 0L);
    }

    public static byte[] initiateRestore(byte[] serialBytes) throws IOException{
        HttpResponse response = HttpConnection.doHttpRequest(
                new HttpRequest(HttpRequest.REQUEST_TYPE_POST, MOBILE_URL+RESTORE_PATH, serialBytes));

        if (response.getResponseCode() != HttpURLConnection.HTTP_OK) {
            throw (new IOException("Restore init request returned HTTP error " + Integer.toString(response.getResponseCode())));
        }
        if (response.getResponseBody().length != 32) {
            throw (new IOException("Restore init request returned invalid HTTP body"));
        }
        return response.getResponseBody();
    }

    public static long getServerTime() throws IOException {
        HttpResponse timeResponse = HttpConnection.doHttpRequest(new HttpRequest(HttpRequest.REQUEST_TYPE_GET, TIME_URL, null));
        if (timeResponse.getResponseCode() != HttpURLConnection.HTTP_OK) {
            throw (new IOException("Time request returned HTTP error " + Integer.toString(timeResponse.getResponseCode())));
        }
        if (timeResponse.getResponseBody().length != 8) {
            throw (new IOException("Time request returned invalid HTTP body"));
        }
        return Util.byteArrayToLong(timeResponse.getResponseBody());
    }

    public static AuthData getAuthenticatorInitialization(String region) throws IOException {
        /* create plain text for request */
        byte[] communicationKey = createOneTimePad(37);
        byte[] regionBytes = DEFAULT_REGION.getBytes();
        byte[] mobileBytes = DEFAULT_MOBILE_MODEL.getBytes();
        byte[] requestData = new byte[1 + communicationKey.length + regionBytes.length + mobileBytes.length];
        requestData[0] = 1;
        System.arraycopy(communicationKey, 0, requestData, 1, communicationKey.length);
        System.arraycopy(regionBytes, 0, requestData, 1 + communicationKey.length, regionBytes.length);
        System.arraycopy(mobileBytes, 0, requestData, 1 + communicationKey.length + regionBytes.length, mobileBytes.length);

        /* do RSA encryption on plain text */
        RSAEngine rsaImplementation = new RSAEngine();
        rsaImplementation.init(true, new RSAKeyParameters(false, ms_modulus, ms_exponent));
        requestData = rsaImplementation.processBlock(requestData, 0, requestData.length);

        /* do the HTTP request with cipher text */
        HttpResponse initializationResponse = HttpConnection.doHttpRequest(new HttpRequest(HttpRequest.REQUEST_TYPE_POST, INITIALIZATION_URL, requestData));
        if (initializationResponse.getResponseCode() != HttpURLConnection.HTTP_OK) {
            throw (new IOException("Initialization request returned HTTP error " + Integer.toString(initializationResponse.getResponseCode())));
        }

        /* parse the response */
        if (initializationResponse.getResponseBody().length != 45) {
            throw (new IOException("Initialization request returned invalid HTTP body"));
        }
        byte[] serverTime = new byte[8];
        System.arraycopy(initializationResponse.getResponseBody(), 0, serverTime, 0, 8);
        byte[] encryptedData = new byte[37];
        System.arraycopy(initializationResponse.getResponseBody(), 8, encryptedData, 0, 37);
        byte[] plainData = doOneTimePadDecryption(encryptedData, communicationKey);
        byte[] secret = new byte[20];
        System.arraycopy(plainData, 0, secret, 0, 20);
        String serial = new String(plainData, 20, 17);
        long offset =  Util.byteArrayToLong(serverTime) - System.currentTimeMillis();
        return new AuthData(region, serial, secret, offset);
    }


    @Deprecated private static byte[] createPseudoOneTimePadKey() {
        /* we do it exactly like Blizzard does for documentation reasons (nevertheless
         * every random 37 bytes would be ok here): SHA1(Random(SystemTime))
         */
        Random random = new Random(System.currentTimeMillis());
        byte[] hashBlock1 = new byte[64];
        byte[] hashBlock2 = new byte[64];
        for(int i = 63; i >= 0; i--) {
            hashBlock1[i] = (byte)(Math.abs(random.nextInt()) % 256);
        }
        for(int i = 63; i >= 0; i--) {
            hashBlock2[i] = (byte)(Math.abs(random.nextInt()) % 256);
        }
        SHA1Digest sha1Implementation = new SHA1Digest();
        byte[] tempKey = new byte[40];
        sha1Implementation.update(hashBlock1, 0, 64);
        sha1Implementation.doFinal(tempKey, 0);
        sha1Implementation.update(hashBlock2, 0, 64);
        sha1Implementation.doFinal(tempKey, 20);
        byte[] key = new byte[37];
        System.arraycopy(tempKey, 0, key, 0, 37);
        return key;
    }

    public static byte[] createOneTimePad(int length)
    {
        SecureRandom random = new SecureRandom();

        byte[] randomBlock = new byte[length];

        random.nextBytes(randomBlock);
        return randomBlock;

    }


    private static byte[] doOneTimePadDecryption(byte[] a_cipherText, byte[] a_key) {
        byte[] plainText = new byte[a_cipherText.length];
        for (int i = 0; i < a_cipherText.length; i++) {
            plainText[i] = (byte)(a_cipherText[i] ^ a_key[i]);
        }
        return plainText;
    }
}
